AWSでジョブWorkerを構成するベストプラクティス 〜 Beanstalk worker tierの巻
よく訓練されたアップル信者、都元です。先日、AWSでジョブWorkerを構成するベストプラクティス 〜 SQSの巻として、SQSを使ったスケーラブルなジョブWorkerアーキテクチャをご紹介しました。人間相手のHTTPリクエストの間に完了させるには長すぎるジョブを実行したい場合は、とりあえずSQSに投げて非同期に処理させよう、という仕組みです。
投げる側は、Javaであればこんな感じですね。キュー毎にURLがあるのでそれを指定して適当なメッセージ(本来はJSONなんかが良いんだと思います)を投げ込みます。
sqs.sendMessage(new SendMessageRequest(QUEUE_URL, "foobar"));
Worker側としては、こんな感じでメッセージを受信しては処理して削除、というのを繰り返せばよいです。
while (true) { ReceiveMessageResult receiveMessageResult = sqs.receiveMessage(new ReceiveMessageRequest(QUEUE_URL)); for (Message message : receiveMessageResult.getMessages()) { String body = message.getBody(); // do something sqs.deleteMessage(new DeleteMessageRequest(QUEUE_URL, message.getReceiptHandle())); } }
ただ、無限ループが出てきたりするのが嫌ですね、また、do somethingの処理が長いと、複数同時に受け取ったのメッセージのうち2つ目以降の実行が遅延することもあるでしょう。などなど、なんとなくスタイリッシュじゃない感があると思います。
Elastic Beanstalk Worker tier
といったWorkerをスタイリッシュに実装する環境として、Elastic Beanstalkの「Worker tier」というのが使えます。
AWS上で動く多くのアプリケーションは、一般的にWebアプリケーションであることが多いと思います。利用者のブラウザやモバイルアプリ等からのHTTPリクエストを受け付けて、HTMLなりJSONなりのレスポンスを返すものです。Elastic BeanstalkはこのようなWebアプリケーションをホストする環境 (Web tier) を提供するサービス、というイメージが強いと思いますが、昨年(2013)の11月より、キューWorkerアプリケーションをホストする環境 (Worker tier) がサポートされるようになりました。Web tierはELBとEC2がセットになったような構成ですが、Worker tierはSQSとEC2がセット売りされる感じです。
で、このWorker tierが実によくできているんです。この環境で提供されるEC2の中ではaws-sqsdというデーモンが動いており、指定したキューのメッセージをポーリングしてくれます。メッセージが配信されてくると、aws-sqsdは、そのメッセージをlocalhostに対して、HTTP POSTしてくれる、という仕組みです。
POSTの結果が200 OKであれば、SQSに対してdelete messageを発行し、そうでなければdelete messageは発行しません。それによって、visibility timeout後に、再び (2) としてメッセージが入ってくることになり、リトライとなります。
というわけで、ジョブの実装を、普段使い慣れたWebアプリケーションとして実装できる、というのがWorker tierの特徴です。
Worker tierにおけるlong-runningジョブ実行
さて、このようなジョブ実行においては、通常のWebアプリケーションよりもlong-runningな処理が実装されがちです。結論から言ってしまうと、この仕組みを「余さずキレイに使いきろう」とした場合、1つのSQSメッセージに相当するジョブ実行時間の最大値は30分です。それ以上のlong-runningジョブには、少々制限があります。
ではまずは、long-runningジョブにかかわる各種タイムアウト項目を見て行きましょう。
SQSのVisibility timeout
SQSにはVisibility timeout *1というものがあります。キューに配信されたメッセージが確実に処理されるように設けられた仕組みです。
ここで、Workerがメッセージを受け取った直後、ジョブを実行する前に障害で死んでしまった場合を考えます。このままそのメッセージが処理されないのは問題です。そこでSQSは、一度配信したメッセージについて、一定時間内に delete message の呼び出しが無かった場合、そのメッセージに関するジョブは実行されなかったと判断し、キュー内にメッセージを復活させます。この一定時間がVisibility timeoutです。
Visibility timeoutを特に指定しなかった場合、SQS上の仕様としてはこの値は1800秒(30分)となります。BeanstalkのWorker tierとしては、この設定値は30秒となっています。つまり、30秒を超えるlong-runnningジョブを実行する場合は、この値をもっと大きい物にしておく必要があります。
ただし、この値を必要以上に長くしてしまうと、原理的に、ジョブ死亡時のリカバリー再実行が遅くなることに注意してください。ちなみにVisibility timeoutの最大値は、43200秒(12時間)です。
Apache httpdのTimeOut
前述の通り、Worker tierにおいては、SQSのメッセージがHTTPリクエストに変換されます。ということは、HTTPのタイムアウトがあります。内部ではApache httpdが動いていますので、そのhttpd.confにおけるTimeOutディレクティブ *2が影響します。
Worker tierにおけるApache httpdでは、デフォルトではこの設定値がありません。すなわち300秒(5分)がタイムアウトとなります。long-runningジョブを実行する場合は、これを延長しておく必要があります。設定可能な最大値はドキュメントに見つかりませんでした。
PHPのmax_execution_time
仮に、このWorkerをPHPで実装すると考えた場合、php.iniにおけるmax_execution_time *3が影響します。この値は0を設定することにより無制限になります。
その他、PHP以外でWorkerを実装する場合は、それぞれの環境における同様のタイムアウトに注意する必要があるでしょう。
Beanstalk worker tierのInactivity timeout
最後に、Beanstalkの設定におけるInactivity timeout *4ですね。これは平たく言えば、aws-sqsdにおけるタイムアウト値です。
この値の最大値は1800秒(30分)です。これが全体のキャップとなっています。この制限が緩和されれば、30分以上のlong-runningジョブが上手く実行できるようになるでしょう。
30分以上のlong-runningジョブ
これをどうしても実行したい場合は、ジョブの完了前に、さっさと200 OKを返してしまい、バックグラウンドで処理を続けるしかありません。この場合、ジョブの完了前にdelete messageが発行されてしまうため、失敗時の再実行の仕組みを別途自分で作り込む必要があるでしょう。
やってみよう
では以上に基いて、実際にBeanstalk worker tierにおけるジョブ実行を試してみましょう。
Workerアプリケーションの準備
まずはPHPによる処理アプリを作りましょう。と言いたいところですが、大変でしょうから適当なものを用意しておきました。本当に適当なのは勘弁してください。まずはwait.phpです。HTTP POSTにおけるbodyの内容を数字だとじて、その秒数だけsleepするという素朴なプログラムです。前後にsyslog出力を行います。
<?php $headers = getallheaders(); $msgid = ""; $fra = ""; $rc = ""; while (list($header, $value) = each($headers)) { if ($header == 'X-Aws-Sqsd-Msgid') { $msgid = $value; } else if ($header == 'X-Aws-Sqsd-First-Received-At') { $fra = $value; } else if ($header == 'X-Aws-Sqsd-Receive-Count') { $rc = $value; } } $sec = file_get_contents('php://input'); syslog(LOG_NOTICE, "start ".$sec." ".$msgid.", ".$fra."-".$rc); sleep($sec); echo $sec." sec wait completed."; syslog(LOG_NOTICE, "end ".$sec." ".$msgid.", ".$fra."-".$rc);
あと、ヘルスチェック用にinfo.phpを用意しています。
<?php phpinfo();
これら2つのファイルをzipで固めたアプリケーションバンドルを作っておきましたので、手元にダウンロードしておいてください。
Beanstalk applicationとenvironmentの作成
まずはBeanstalkのManagement Consoleへ。右上の「Create New Application」をクリックします。
Application名は任意で。ここでは「worker-sample」としました。
次に動作させるアプリケーションバンドルの指定です。「Upload your own」を選択し、先ほどダウンロードしておいたzipファイルを指定します。
次にEnvironmentの設定です。今回はWeb tierではなくWorker tierですね。プラットフォームはPHP、一応AutoScaling構成にしておきましょう。
次の画面は特にいじらず。本格運用する場合は、RDSも必要になるでしょうし、VPC内にインスタンスをたちあげたくなると思いますが。
environment名も任意で。ここでは「worker-production」としました。
タグ指定も任意です。ここでは何も触りませんでした。
次に、aws-sqsdの設定です。既存のキューの出口に並べる場合はそのキューを指定します。今回は自動新規作成を選びました。また、キューメッセージをポストする先のパスを指定します。今回は/wait.phpですね。また、POST時のMIME typeは何でも構いません。デフォルトでapplication/jsonとなっています。今回のbodyは単純な数字のつもりなので、なんとなくtext/plainにしておきました。
続いて、EC2インスタンスの設定です。検証ですのでインスタンスタイプはt1.microで充分でしょう。キーペアは必ず指定しておいてください。コレがないとSSH接続してsyslogを観察できません。ヘルスチェックは/info.phpです。Instance Profileも(あれば)適当なものを指定しておきましょう。Beanstalkは、ログファイルを定期的にS3にアップロードする機能がありますが、Instance Profileが無いとアップロードができません。
最後に設定をひと通りレビューし、「Launch」ボタンをクリックします。
環境構築が始まります。
10分弱、このまま待機すると、 イベントログに現在の状況が次々に出てくるのがわかると思います。「Environment health has been set to GREEN」というイベントが現れたら構築完了です。
EC2インスタンスとしても、サーバが立ち上がっているのが確認できます。
Workerの動作検証
では、色々触ってみましょう。上記EC2インスタンスのPublic IPアドレスを確認し、SSH接続でログのウォッチを始めます。
$ sudo tail -f /var/log/messages
この状態で、BeanstalkのManagement Consoleから、「View Queue」というリンクをクリックします。
今回の環境用に作成されたSQSキューが見えるはずです。
では、このキューを選択(チェック)した状態で「Queue Actions」メニューから「Send Message」を選択します。
現れたポップアップに、メッセージを記入します。まず10秒waitさせてみましょう、ということで「10」で「Send Message」ボタンをクリックすると…。その瞬間にログをウォッチしているコンソールにメッセージが出るはずです。
May 16 02:37:25 ip-XXX-XX-X-XXX httpd: start 10 92c980e4-XXXX-XXXX-XXXX-504b18e95419, 2014-05-16T02:37:25Z-1 May 16 02:37:35 ip-XXX-XX-X-XXX httpd: end 10 92c980e4-XXXX-XXXX-XXXX-504b18e95419, 2014-05-16T02:37:25Z-1
startから10秒後にendのログが出ていますね。あとは色々なメッセージを送ってみてください。
おかたづけ
さて、検証が終わったら片付けておきましょう。BeanstalkのManagement Consoleより、当該アプリケーションのActionsメニューから「Delete Application」を選ぶだけです。撤収にまた10分弱かかりますが、気長に待ちましょう。
まとめ
スクショ多めでお送りいたしました(疲れた)。
このように、AWSでは、というよりBeanstalkでは、スケーラブルなジョブWorkerを簡単に実装できます。キューに溜まったメッセージ数に応じてAutoScalingでWorkerの数を増減させる、なんてことも簡単に出来てしまいます。私はサンプルだけでなく実務でも利用していますので、皆さんのシステムでも是非、バックグラウンドジョブの実行の仕組みとしてBeanstalkのWorker tierを使ってみてください。
脚注
- http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/AboutVT.html ↩
- http://httpd.apache.org/docs/2.2/mod/core.html#timeout ↩
- http://php.net/manual/ja/info.configuration.php#ini.max-execution-time ↩
- http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features-managing-env-tiers.html ↩